Lab 4: Preemptive Multitasking

这个实验中,我们将会实现一个抢占式(preemptive)的多核系统。

实验整体的思维导图:

Part A: Multiprocessor Support and Cooperative Multitasking

part A我们将拓展原来的JOS系统,使之变成一个多核系统。

之后实现一些system call,使得能够在用户态建立一些新的environments。

然后实现一个cooperative round-robin调度算法,当某个environment主动退出CPU的使用的时候,其他进程(后面默认进程 == environment)接着使用。

最后,我们会实现一个抢占式的调度算法,当某个进程执行一定的时间(不一定执行完了),使得内核能够重新使用CPU。

Multiprocessor Support

在启动的过程中,我们可以将CPU分成两类:

  • the bootstrap processor (BSP):主要负责内核的启动
  • the application processors(APs):是被BSP启动的。

至于哪一个CPU核是BSP,则是被硬件和BIOS所确定的。

在SMP(symmetric multiprocessing)系统中,每一个CPU都会有一个local APIC(advanced programmable interrupt controller)。这些LAPIC主要负责传递一些中断信号。每一个单元都会有一个unique identifier。我们将会使用LAPIC单元的这些功能。

  • 读取LAPIC id,从而知道我们现在此刻代码是跑在哪一个CPU中的。
uint8_t cpu_id;                 // Local APIC ID; index into cpus[] below
  • BSP发送STARTUPinterprocessor interrrupt(IPI)给APs,使得能够启动其他的CPU。(见 lapic_startap()
  • 在part C,我们使用内置的时钟中断,使得能够支持抢占式多任务。

一个处理器通过memory-mapped I/O(MMIO)来获取它的LAPIC,相当于是直接通过读写内存来操控硬件(虽然看起来像是操作内存,但实际上还是和硬件中的存储进行交互)。之前我们知道在最开始的1MB内存中0xA0000的物理地址开始存放的是VGA display buffer。

exercise 1

实现kern/pmap.c中的mmio_map_region。

我们读取到CPU的配置信息后,里面有LAPIC单元的物理地址(lapicaddr),我们需要做的就是将虚拟地址MMIOBASE映射到这一部分的物理地址。

void *
mmio_map_region(physaddr_t pa, size_t size)
{
    // Where to start the next region.  Initially, this is the
    // beginning of the MMIO region.  Because this is static, its
    // value will be preserved between calls to mmio_map_region
    // (just like nextfree in boot_alloc).
    // 这个static很关键啊,这样就不用保存为全局变量了。
    static uintptr_t base = MMIOBASE;


    // write through: 同时更新cache和后端的存储中的数据
    // write back: 仅仅更新cache中的数据,必要时才往后端的设备写回数据

    // Your code here:
    if(base + ROUNDUP(size, PGSIZE) > MMIOLIM)
        panic("mmio_map_region: mmio overflow.");
    boot_map_region(kern_pgdir, base, ROUNDUP(size, PGSIZE), pa, PTE_W|PTE_PCD|PTE_PWT);
    uintptr_t temp = base;
    base += ROUNDUP(size, PGSIZE);
    return (void *)temp;
    panic("mmio_map_region not implemented");
}

mmio是一块很特殊的内存区域,能够像访问内存一样,去访问硬件的寄存器,但是又和一般的内存有不同的地方,一般内存都带有缓存的功能,也就是将内存的内容缓存到CPU的cache中。由于设备寄存器的多变性,我们需要将内存的缓存功能关闭。幸运的是,内存的权限位提供这样的支持,需要将相应的权限为设置为PTE_PCDPTE_PWT即可,也就是cache disable和write through。

Application Processor Bootstrap

在启动APs,我们需要读取从BIOS区域读取一些信息,如CPU的数量,APIC ID和MMIO物理地址。

驱动APs的函数在boot_aps中。APs的启动在实模式下,和boot loader的启动方式很像。因此boot_aps()将要执行的代码复制到实模式能够寻址到的地方(实际上是放在了0x7000 (MPENTRY_PADDR))。实际上任何放在640KB以下的内存都是没有问题的。

// Write entry code to unused memory at MPENTRY_PADDR
code = KADDR(MPENTRY_PADDR);
memmove(code, mpentry_start, mpentry_end - mpentry_start);

放置好要执行的代码之后,boot_aps依次激活APs,首先发送一个STARTUP的IPI中断,然后设置CS:IP寄存器。

之后APs类似于boot loader部分执行初始化代码,然后运行mp_main()初始化一些寄存器的值,如GDT,TSS等等。BSP会等待APs的 CPU_STARTED信号,收到了才会激活其他的APs。

exercise 2

阅读相关部分的代码,修改kern/pmap.c中的page_init函数,将MPENTRY_PADDR的页从page_free_list中移除。

for (i = 1; i < npages_basemem; i++) {
        if(i == MPENTRY_PADDR/PGSIZE){
            pages[i].pp_ref = 1;
            continue;
        }
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
}

Q1: 对比kern/mpentry.Sboot/boot.S,记住kern/mpentry.S始终是运行在KERNBASE地址之上的。那么在kern/mpentry.S中的MPBOOTPHYS宏定义的目的是什么?为什么在kern/mpentry.S是必要的,而在boot loader中不需要?如果去掉有什么错误?

A:boot.S在实模式下的,而重新进入时,mpentry.S是在保护模式下进行的。因此我们必须重新将其转化为实际的物理内存地址。

Per-CPU State and Initialization

当实现一个多核系统的时候,区分私有的状态还是共享的状态是非常重要的。

我们需要知道每一个CPU下面的一些状态:

  • per-CPU kernel stack

每一个CPU的栈内容保存在percpu_kstacks[NCPU][KSTKSIZE]。我们需要将虚拟地址从KSTACKTOP开始向下依次的进行映射。

并且不同CPU栈中间是有一块隔离区的。

  • per-CPU TSS 和 TSS descriptor

前面我们提到TSS结构主要是保存处理器旧的状态,方便用户态跳转回内核态。现在多核系统中是保存CPU的栈和若干寄存器的状态,具体使用cpus[i].cpu_ts变量。TSS descriptor保存在个GDT中,通过gdt[(GD_TSS0 >> 3) + i]进行索引。

  • per-CPU目前执行的进程

通过cpus[cpunum()].cpu_env进行索引。

  • per-CPU system registers

所有的寄存器,包括系统寄存器,对CPU都是隐秘的(感觉这里讲的不清楚,应该每一个CPU都有属于自己的寄存器,相互独立不能访问,因此需要进行初始化)。因此我们需要使用一些函数lcr3(), ltr(), lgdt(), lidt()进行初始化。

exercise 3

修改在kern/pmap.c中的mem_init_mp(),从而将虚拟地址与物理地址进行相应的映射。

mem_init_mp(void)
{
    // Map per-CPU stacks starting at KSTACKTOP, for up to 'NCPU' CPUs.
    //
    // For CPU i, use the physical memory that 'percpu_kstacks[i]' refers
    // to as its kernel stack. CPU i's kernel stack grows down from virtual
    // address kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP), and is
    // divided into two pieces, just like the single stack you set up in
    // mem_init:
    //     * [kstacktop_i - KSTKSIZE, kstacktop_i)
    //          -- backed by physical memory
    //     * [kstacktop_i - (KSTKSIZE + KSTKGAP), kstacktop_i - KSTKSIZE)
    //          -- not backed; so if the kernel overflows its stack,
    //             it will fault rather than overwrite another CPU's stack.
    //             Known as a "guard page".
    //     Permissions: kernel RW, user NONE
    //
    // LAB 4: Your code here:
    uint32_t i=0;
    uintptr_t start = KSTACKTOP-KSTKSIZE;
    for(; i<NCPU; i++){
        boot_map_region(kern_pgdir, start, KSTKSIZE, PADDR(percpu_kstacks[i]), PTE_W);
        start -= (KSTKSIZE+KSTKGAP);
    }

}

KSTKGAP起到的作用就是隔离各CPU的栈空间,防止相互干扰。

exercise 4

在lab3 中,我们使用的是全局的ts来保存旧的一个处理器的状态。现在我们对每一个CPU都保存一个ts。

// Initialize and load the per-CPU TSS and IDT
void
trap_init_percpu(void)
{
    // Setup a TSS so that we get the right stack
    // when we trap to the kernel.
    //ts.ts_esp0 = KSTACKTOP;
    //ts.ts_ss0 = GD_KD;
    //ts.ts_iomb = sizeof(struct Taskstate);
    struct Taskstate *thists = &thiscpu->cpu_ts;
    thists->ts_esp0 = KSTACKTOP - thiscpu->cpu_id * (KSTKSIZE + KSTKGAP);
    thists->ts_ss0 = GD_KD;
    thists->ts_iomb = sizeof(struct Taskstate);

    // Initialize the TSS slot of the gdt.
    gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id] = SEG16(STS_T32A, (uint32_t) (thists),
                    sizeof(struct Taskstate) - 1, 0);
    gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id].sd_s = 0;

    // Load the TSS selector (like other segment selectors, the
    // bottom three bits are special; we leave them 0)
    ltr(GD_TSS0+ (thiscpu->cpu_id << 3));

    // Load the IDT
    lidt(&idt_pd);
}

Locking

在让APs进一步执行前,我们需要解决内核竞争的问题防止多个CPU同时运行内核代码。

目前设置一个大锁,使得有一个CPU进入内核时,那么就锁住内核,仅允许一个CPU运行。当返回到用户态时,就释放该锁。

因此,上面的大锁设计,使得用户态的程序能够多CPU的运行,仅能有一个进程运行内核,其他的处理器进程都得等待。

我们需要在下面的四个地方运用内核锁:

  • i386_init()中唤起其他APs前需要锁定内核
  • mp_main()中尝试着索取内核锁,从而能够进行进程的调用。
  • trap()中从用户态陷入到内核态,尝试着去获取锁。
  • env_run()env_pop_tf()前释放锁。

我们看到仅有一处是锁的释放的,那就是从内核态进入到用户态的前夕。

exercise 5

在合适的位置运行lock_kernelunlock_kernel

目前还无法测试锁的正确性。

几处加锁:

  1. 一个CPU尝试着启动其他的CPU的时候
// Lab 4 multiprocessor initialization functions
    mp_init();
    lapic_init();

    // Lab 4 multitasking initialization functions
    pic_init();

    // Acquire the big kernel lock before waking up APs
    // Your code here:
    lock_kernel();
    // Starting non-boot CPUs, mpentry.S 入点
    boot_aps();
  1. 从用户态陷入到内核态:
if ((tf->tf_cs & 3) == 3) {
        // Trapped from user mode.
        // Acquire the big kernel lock before doing any
        // serious kernel work.
        // LAB 4: Your code here.
        lock_kernel();
        assert(curenv);
}

一处解锁:

  1. 从内核到用户态:
void
env_run(struct Env *e)
{
     curenv->env_status = ENV_RUNNING;
    ++curenv->env_runs;
    lcr3(PADDR(curenv->env_pgdir));
    unlock_kernel();
    env_pop_tf(&curenv->env_tf);
}

Q2: 目前的锁机制,使得每一次只有一个CPU运行内核代码,为什么我们仍然需要将CPU栈分离开?如果不分开有什么错?

A:lab3中,内核态->系统态一定是从栈的最顶端开始的(KSTACKTOP),因此每一次陷入的时候都像是一个新的从未使用的内核栈。这样看的话,好像是能够进行公用的。但是不要忘了朋友们,后面的抢占式进行调度,那么内核栈中和有可能保存着我们需要的信息,或者是上次未执行完的信息。如果调用system call那么压入的参数就不一样啊,显然不能使用同一个栈。

Round-Robin Scheduling

Round-Robin就是循序的进行调度,每一个进程调度的都是均等的。

执行的步骤如下:

  • sched_yield()挑选一个进程进行执行,通过环形的方式对envs[]进行访问,挑选第一个状态为RUNNABLE的进程放入到目前CPU中进行执行。
  • sched_yield()不能在两个CPU上运行同一个进程。
  • 系统已经提供了一个新的系统调用sys_yield(),使得用户态进程能够主动的调用sched_yeild()并且让其他的进程被CPU执行。

exercise 6

完成sched_yield()函数的实现,实现Round-Robin调度

首先完成调度的程序:

// Choose a user environment to run and run it.
void
sched_yield(void)
{
    struct Env *idle;

    // Implement simple round-robin scheduling.
    //
    // Search through 'envs' for an ENV_RUNNABLE environment in
    // circular fashion starting just after the env this CPU was
    // last running.  Switch to the first such environment found.
    //
    // If no envs are runnable, but the environment previously
    // running on this CPU is still ENV_RUNNING, it's okay to
    // choose that environment.
    //
    // Never choose an environment that's currently running on
    // another CPU (env_status == ENV_RUNNING). If there are
    // no runnable environments, simply drop through to the code
    // below to halt the cpu.

    // LAB 4: Your code here.
    int counter;

    if (curenv) {
        for (counter = ENVX(curenv->env_id) + 1;
                counter != ENVX(curenv->env_id);
                counter = (counter + 1) % NENV){
            //cprintf("%d\n", counter);
            if (envs[counter].env_status == ENV_RUNNABLE){
                env_run(envs + counter);
            }
        }
        if(curenv->env_status != ENV_NOT_RUNNABLE)
            env_run(curenv);
            //cprintf("%d\n", counter);
    } else {
        for (counter = 0; counter < NENV; ++counter)
            if (envs[counter].env_status == ENV_RUNNABLE)
                env_run(envs + counter);
    }
    // sched_halt never returns

    sched_halt();
}

然后,在syscall 中添加一个case 来使用sys_yield 系统调用:

case SYS_yield:
    sys_yield();
    return 0;

之后在i386_init()函数中加入以下的代码:

ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);

输入make qemu CPUS=2就能看到下面的输出结果了:

Hello, I am environment 00001000.
Hello, I am environment 00001001.
Hello, I am environment 00001002.
Back in environment 00001000, iteration 0.
Back in environment 00001001, iteration 0.
Back in environment 00001002, iteration 0.
Back in environment 00001000, iteration 1.
Back in environment 00001001, iteration 1.
Back in environment 00001002, iteration 1.
...

Q3:在env_run()中使用了lcr3()用来更新页目录,因此MMU内容会立刻被更新。那么为什么更新前后当前准备运行的environment虚拟地址映射到物理地址没有变化?

因为在进程页目录初始化时,复制的就是内核的页目录:

e->env_pgdir = (pde_t *)page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);

仅仅在UVPT这个虚拟地址有了修改,具体可以看看env_create.env_alloc.env_setup_vm

这个问题应该在lab3如何初始化environment就能够回答。

Q4:当CPU从一个environment转移到了另外一个environment,系统必须要保存旧的environment的寄存器使得它之后能够正确的被再次唤起。这个是在哪里进行的。

在env->env_tf中保存的,也就是Trapframe结构。保存是发生在_alltraps构造Trapframe,我们已经在lab3中详细的讨论过了。恢复发生在kern/env.c 中的env_pop_tf处。

System Calls for Environment Creation

现在能进行进程的切换,但是运行的进程数在系统初始化的时候就已经确定好了。下面的实现就是为了能够在用户态创建新的environment。

Unix使用fork()来进行创建。该函数会赋值整个进程的地址空间作为child process。parent process和child process的区别就是process ID。在parent进程中,fork返回child ID,在child process中,fork返回0(environment->env_tf.tf_regs.reg_eax = 0;)。

在JOS中,我们需要实现下面的system call:

  • sys_exofork:

这个系统调用会产生一个空白的进程空间。

调用的parent env会获得子进程的进程号,而子进程得到的数值为0。

  • sys_env_set_status: 当初始化好了若干的页面的设置,那么将进程的状态ENV_NOT_RUNNABLE改位ENV_RUNNABLE
  • sys_page_alloc: 根据相应的虚拟地址,分配相应的物理内存。

  • sys_page_map:

感觉是一种内存共享的方式,两个进程的页目录映射有同一块地址空间。

  • sys_page_unmap:

与上面的相反。

上面的所有系统调用函数参数中都会包括environment IDs。JOS提供ID到environment的转换,使用envid2env()

特别需要注意的是,envid2env(0)返回的是当前environment的指针。

JOS此刻已经提供了一个简陋的像fork()那样的实现dumbfork()(为什么说简陋因为没有copy on write的机制)。我们需要实现上面的system call使得能够实现这个简陋的dumbfork()。

exercise 7

实现上面的system call。

当调用envid2env函数的时候,checkperm=1,使得我们始终检查environment关系。

检查所有的系统调用,如果参数不正确或者不在规定的范围,那么就返回-E_INVAL

首先实现sys_exofork():

// Allocate a new environment.
// Returns envid of new environment, or < 0 on error.  Errors are:
//    -E_NO_FREE_ENV if no free environment is available.
//    -E_NO_MEM on memory exhaustion.
static envid_t
sys_exofork(void)
{
    // Create the new environment with env_alloc(), from kern/env.c.
    // It should be left as env_alloc created it, except that
    // status is set to ENV_NOT_RUNNABLE, and the register set is copied
    // from the current environment -- but tweaked so sys_exofork
    // will appear to return 0.

    // LAB 4: Your code here.
    struct Env *environment;
    int res;
    if((res = env_alloc(&environment, curenv->env_id)) < 0)
        return res;
    environment->env_status = ENV_NOT_RUNNABLE;
    environment->env_tf = curenv->env_tf;
    environment->env_tf.tf_regs.reg_eax = 0;//这应该就是子进程的返回值了
    return environment->env_id;
    //panic("sys_exofork not implemented");
}

之后实现sys_page_alloc():

static int
sys_page_alloc(envid_t envid, void *va, int perm)
{
    // Hint: This function is a wrapper around page_alloc() and
    //   page_insert() from kern/pmap.c.
    //   Most of the new code you write should be to check the
    //   parameters for correctness.
    //   If page_insert() fails, remember to free the page you
    //   allocated!

    // LAB 4: Your code here.
    if(!(perm & PTE_U) || !(perm & PTE_U) ||
            (perm & (~PTE_SYSCALL)) ||
            va > (void *)UTOP ||
            va != ROUNDDOWN(va, PGSIZE))
        return -E_INVAL;
    struct PageInfo *pginfo = page_alloc(ALLOC_ZERO);
    if(!pginfo)
        return -E_NO_MEM;
    struct Env *environment;
    if(envid2env(envid, &environment, 1) < 0)
        return -E_BAD_ENV;
    if(page_insert(environment->env_pgdir, pginfo, va, perm) < 0){
        page_free(pginfo);
        return -E_NO_MEM;
    }
    return 0;
    //panic("sys_page_alloc not implemented");
}

sys_page_map实现:

static int
sys_page_map(envid_t srcenvid, void *srcva,
         envid_t dstenvid, void *dstva, int perm)
{
    // Hint: This function is a wrapper around page_lookup() and
    //   page_insert() from kern/pmap.c.
    //   Again, most of the new code you write should be to check the
    //   parameters for correctness.
    //   Use the third argument to page_lookup() to
    //   check the current permissions on the page.

    // LAB 4: Your code here.
    if((uint32_t)srcva >= UTOP || PGOFF(srcva) ||
            (uint32_t)dstva >= UTOP || PGOFF(dstva) ||
            !(perm & PTE_U) ||
            (perm & (~PTE_SYSCALL)) )
        return -E_INVAL;
    struct Env *src_environemt, *dst_environment;
    if(envid2env(srcenvid, &src_environemt, 1) < 0 ||
            envid2env(dstenvid, &dst_environment, 1) < 0)
        return -E_BAD_ENV;
    pte_t *pte;
    struct PageInfo *page = page_lookup(src_environemt->env_pgdir, srcva, &pte);
    // if(srcenvid == 4097)
    //     *pte |= PTE_W;
    if(!page || (!(*pte & PTE_W) && (perm & PTE_W)))
        return -E_INVAL;
    if(page_insert(dst_environment->env_pgdir, page, dstva, perm) < 0)
        return -E_NO_MEM;
    return 0;
    //panic("sys_page_map not implemented");
}

sys_page_unmap:

static int
sys_page_unmap(envid_t envid, void *va)
{
    // Hint: This function is a wrapper around page_remove().

    // LAB 4: Your code here.
    if((uint32_t)va >= UTOP || PGOFF(va))
        return -E_INVAL;
    struct Env *environment;
    if(envid2env(envid, &environment, 1) < 0)
        return -E_BAD_ENV;
    page_remove(environment->env_pgdir, va);
    return 0;
    //panic("sys_page_unmap not implemented");
}

最后增加这几个系统调用的分发:

        case SYS_exofork:
            return sys_exofork();
        case SYS_env_set_status:
            return sys_env_set_status(a1, a2);
        case SYS_page_alloc:
            return sys_page_alloc(a1, (void *) a2, a3);
        case SYS_page_map:
            return sys_page_map(a1, (void *) a2, a3, (void *) a4, a5);
        case SYS_page_unmap:
            return sys_page_unmap(a1, (void *) a2);

Part B: Copy-on-Write Fork

Unix提供fork()来在用户态进行进程的创建。

vx6通过赋值所有parent的页进入新的页,从而创建新的进程。并且这样的复制行为代价也是整个fork()开销最大的操作。

然而,一个fork()函数调用后往往跟随着一个exec()函数,又需要将原来的内存替换为其他内容。因为child process对parent process拷贝后的内容用的非常的少,因此将整个页内容都拷贝过来将是是非浪费效率的一种行为。

正是上面遇到的这种问题,之后的unix利用虚拟内存的硬件,使得parent和child process能够共享这一部分的内存,直到其中的一个进程修改了页的内容,就会结束共享页的行为,这种行为叫做copy-on-write。

为了能够实现上面的功能,fork()实现时,内核将会赋值parent的address space mappings,也就是页目录和页表到child process,而不将内容拷贝到child process中,并且将内容权限设置为read-only。当其中的一个进程尝试对内存进行写的操作的时候,那么将会发生page falut中断。并且分配新的页,这些内容设置为可写。

上面描述的方式使得一般fork()操作的开销非常的小,一般只用复制1page(4096Bytes)。

User-level page fault handling

为了能够实现上面的copy-on-write,我们首先需要知道在read-only的页上会发生page fault的错误。

并且发生page fault这种时间也是非常常见的,比如一般内核在初始化程序的时候,仅会分配一个页,当栈空间不够的时候,那么就会发生page fault。同理,这些事件也会发生在BSS区域,并且初始化的时候会全部赋值为0。当需要执行的指令不在内存的时候,也会发生page fault,从而能够从磁盘读取相关指令。可见page fault是一个非常常见的事件,并且是一个很好优化系统性能的手段。

JOS中,我们并不会在内核态封装page fault handler,相反,我们在用户态可以自由的定制page fault handler。我们需要好好的设计page fault的处理机制,从而能够灵活的处理上面提到的多种发生page fault情况的事件。

之后我们需要处理获取从硬盘上的文件系统的内容。

Setting the Page Fault Handler

为了能够用户态有自己的page fault handler,我们需要在JOS内核的page fault handler entrypoint进行赋值确定(就是函数指针的赋值)。用户态进程通过sys_env_set_pgfault_upcall注册自己的系统调用。并且在Env结构体中增加了一个新的变量env_pgfault_upcall用来记录这个值。

exercise 8

实现sys_env_set_pgfault_upcall,确保权限的检查,使得只有相应的进程ID才能进行修改,因为这是一个非常危险的系统调用。

kern/syscall.c中进行实现:

// Set the page fault upcall for 'envid' by modifying the corresponding struct
// Env's 'env_pgfault_upcall' field.  When 'envid' causes a page fault, the
// kernel will push a fault record onto the exception stack, then branch to
// 'func'.
//
// Returns 0 on success, < 0 on error.  Errors are:
//    -E_BAD_ENV if environment envid doesn't currently exist,
//        or the caller doesn't have permission to change envid.
static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
    // LAB 4: Your code here.
    struct Env *environment;
    if(envid2env(envid, &environment, 1))
        return -E_BAD_ENV;
    environment->env_pgfault_upcall = func;
    return 0;
    // panic("sys_env_set_pgfault_upcall not implemented");
}

并且在/kern/syscall.c/syscall()中进行分发:

case SYS_env_set_pgfault_upcall:
            return sys_env_set_pgfault_upcall(a1, (void *) a2);

Normal and Exception Stacks in User Environments

正常用户态的栈空间在[USTACKTOP-1, USTACKTOP-PGSIZE]之间。

当用户态产生中断,内核将会在一个设定好的栈空间重启用户进程,也就是在user exception stack进行处理。并且这个过程和用户态切换到内核态原理基本是相同的。

user exception stack也是一个页的大小,并且栈底的位置在UXSTACKTOP。然后在这个栈空间,能够正常的调用系统调用,从而能够对页进行重新的映射,分配或者是修复任何与page fault有关问题。user-level page handler通过汇编语言stub返回相应的值,也是原来栈上面填错误码(uint32_t tf_trapno)的地方。

每一个进程如果想要支持用户态的page fault handler,必须自己分配相应的异常处理栈,可以使用sys_page_alloc()进行分配。

Invoking the User Page Fault Handler

构造page fault handler栈如下面结果所示:

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

/inc/trap.h里面结构体变量结构一致:

struct UTrapframe {
    /* information about the fault */
    uint32_t utf_fault_va;    /* va for T_PGFLT, 0 otherwise */
    uint32_t utf_err;
    /* trap-time return state */
    struct PushRegs utf_regs;
    uintptr_t utf_eip;
    uint32_t utf_eflags;
    /* the trap-time stack to return to */
    uintptr_t utf_esp;
} __attribute__((packed));

当中断处理未完成时,可以进行嵌套的操作,不过此时栈的变化是从此刻的tf->tf_eps向下增长的,而不是从UXSTACKTOP开始向下变化,反正是可以进行嵌套处理的。

确保异常处理栈不能超过空间大小,因为这个栈的下面就是用户进程栈,如果覆盖了那么即使异常正确的处理了,返回后程序依旧不能够正确的执行。

exercise 9

实现/kern/trap.c中的page_fault_handler,该函数能够分发用户态的page fault handler。

确保正确的对栈空间进行操作

如果 exception stack使用的空间超过了会发生什么?(上面回答了)

通过该函数的注释,我们需要对page fault进行分类讨论处理:

  • 当初次发生page fault handler时,这个栈陷入的变化时用户栈->内核栈->异常处理栈(返回时异常栈->直接用户栈)。
  • 当有page fault handler的嵌套时,此时已经在异常处理栈了,此时需要再次压入一个UTrapframe,并且之间空4Bytes,此时栈陷入顺序为:异常处理栈->内核栈->异常处理栈(注意返回顺序并不是这样的,而是直接异常栈->一场栈)。

并且我们要时刻保证压入的UTrapgrame没有超过内存限制大小,最终的实现代码如下:

// LAB 4: Your code here.
if(curenv->env_pgfault_upcall){
        struct UTrapframe *utf;
        uintptr_t addr;         // addr of utf
        if(UXSTACKTOP - PGSIZE <= tf->tf_esp && tf->tf_esp < UXSTACKTOP)
            addr = tf->tf_esp - sizeof(struct UTrapframe) - 4;
        else
            addr = UXSTACKTOP - sizeof(struct UTrapframe);
        user_mem_assert(curenv, (void *)addr, sizeof(struct UTrapframe), PTE_W);
        utf = (struct UTrapframe *)addr;
        utf->utf_fault_va = fault_va;
        utf->utf_err = tf->tf_err;
        utf->utf_regs = tf->tf_regs;
        utf->utf_eip = tf->tf_eip;//用户能够返回到用户态的设置
        utf->utf_eflags = tf->tf_eflags;
        utf->utf_esp = tf->tf_esp;

        tf->tf_eip = (uint32_t)curenv->env_pgfault_upcall;
        tf->tf_esp = addr;
        env_run(curenv);
}

User-mode Page Fault Entrypoint

接下来,我们需要实现汇编代码,来对page fault正确的进行处理,并且处理完后能够正确的返回当前函数执行的位置。这个assembly routine将会在sys_env_set_pgfault_upcall()上register(赋值)上。

exercise 10

实现lib/pfentry.S中的_pgfault_upcall函数。

这一部分比较有趣的是能够返回用户态异常地址的地方,不需要通过内核进行返回。(注意陷入和返回的过程是不一样的)

最难的部分是同时变换栈和重新加载EIP。(此处同时变换不是指内存变换,而是再还原的过程中,有的寄存器一旦被还原,就不能再继续被使用了)

关于调用page_fault_handler过程比较的简单:

  1. 用UTrapframe来保存当前用户态的寄存器的值,这部分的值存储在异常栈处理部分UXSTACKTOP
  2. 更新用户态的Tramframe中的EIP=page_fault_handler和ESP(异常栈部分)
  3. _pgfault_upcall调用用户自己定义的page_fault_handler,其中传入参数UTrapframe(pushl %esp)。
  4. 处理完后,我们直接从异常栈跳转到用户态(而不用通过内核态进行中转)。

这个跳转的部分就是这个实现比较难的部分了,下面讲讲具体的思路。

假设现在异常栈只压入了一个栈帧,此刻的栈长成这样:

                    <-- UXSTACKTOP
                    4 Bytes
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

首先执行因为栈顶的两个数据并不影响寄存器的值,不需要进行还原,addl $0x8, %esp,栈内容变成这样:

                    <-- UXSTACKTOP
                    4 Bytes
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       <-- %esp when handler is run

之后执行下面语句:

subl $0x4, 0x28(%esp)
movl 0x28(%esp), %edx

栈中的内容变成了这样:

                    <-- UXSTACKTOP
                    4 Bytes
trap-time esp-4
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       <-- %esp when handler is run

运行下面的指令:

movl 0x20(%esp), %eax
movl %eax, (%edx)

栈中内容变成了:

                    <-- UXSTACKTOP
trap-time eip       4 Bytes //这里发生了变化
trap-time esp-4
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       <-- %esp when handler is run

经历下面的指令:

popal
addl $0x4, %esp
popfl
popl %esp

栈中的内容变成了:

                    <-- UXSTACKTOP
trap-time eip       4 Bytes

最后执行ret,将栈顶的内容赋值给EIP,最终完成了跳转。

异常栈在分配空间的时候,会额外的多分配4Bytes,之前一直以为起到的是隔离的作用,不过最后才发现为了ret 给EIP赋正确的值。

最终合起来的代码:

addl $0x8, %esp

subl $0x4, 0x28(%esp)
movl 0x28(%esp), %edx

movl 0x20(%esp), %eax
movl %eax, (%edx)

popal
addl $0x4, %esp
popfl
popl %esp

ret

我们简单的总结一下,page_fault_handle的栈变化情况:

陷入:用户栈->内核栈->异常栈

返回:异常栈->用户栈

如果是嵌套处理,效果也是类似的,返回中都不会经过内核栈,若page fault嵌套,那么返回过程为:

异常栈帧n->异常栈帧n-1 ... ->用户栈

exercise 11

实现/kern/pgfault.c中的set_pgfault_handler函数

set_pgfault_handler可以在用户态进行调用,并且可以在用户态自由定义处理函数。

//
// Set the page fault handler function.
// If there isn't one yet, _pgfault_handler will be 0.
// The first time we register a handler, we need to
// allocate an exception stack (one page of memory with its top
// at UXSTACKTOP), and tell the kernel to call the assembly-language
// _pgfault_upcall routine when a page fault occurs.
//
void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
    int r;

    if (_pgfault_handler == 0) {
        // First time through!
        // LAB 4: Your code here.
         envid_t envid = sys_getenvid();
        if(sys_page_alloc(envid, (void *)(UXSTACKTOP-PGSIZE), PTE_U | PTE_W | PTE_P) < 0)
            panic("set_pgfault_handler: sys_page_alloc failed. ");
        if(sys_env_set_pgfault_upcall(thisenv->env_id, _pgfault_upcall) < 0)
            panic("set_pgfault_handler: sys_env_set_pgfault_upcall failed.");
        //panic("set_pgfault_handler not implemented");
    }

    // Save handler pointer for assembly to call.
    _pgfault_handler = handler;
}

Implementing Copy-on-Write Fork

通过上面的实现,目前我们已经在用户态完成了copy-on-write必要的函数功能。

在fork()的实现中,也会在child process复制parent process整个地址空间。和dumbfork()不同的是,fork()只有当某个进程尝试对页进行写操作时,才会创建新的页,使其能够进行写操作。

整个fork()流程基本如下:

  1. parent process通过set_pgfault_handler()建立pgfault()。

  2. parent process 使用sys_exofork()去创建一个新的child process。

  3. 在parent process中UTOP以下的内存部分,若权限是可写(writable)或者是copy-on-write权限,那么就使用duppage。修改的步骤为:①将这些页映射到child process并且权限设置为copy-on-write。②重新将这些页权限改为copy-on-write(注意先设置child process再设置parent process是非常重要的,设想一个调换顺序的反例:------------)。duppage权限使用PTEs|PTE_COW来区分那些真正只读的页。

    需要给child process分配一个新的异常处理栈,因为我们要对中断一场进行处理,因此不能对其使用COW。

  4. parent process为child process设置user page fault handler entrypoint。

  5. 将一开始创建的新child process ENV_NOT_RUNNABLE设置为ENV_RUNNABLE

每当其中的一个进程对这些COW页进行写操作时,就会发生page fault。下面讲讲user page fault handler的控制流:

  1. 发生中断陷入内核,trap()会设置UTrapframe()并且陷入异常处理栈,从而能够调用pgfault() handler。
  2. pgfault()会检测这些页是否可写,或者时权限设置为COW的页,如果不是上面的两种权限,那么报错。
  3. pgfault()在首先在PFTEMP分配映射一个物理页,然后将之前的页内容复制到这个新分配的物理页中,重新对映射虚拟地址,从而完成新页面的设置。

exercise 12

实现/lib/fork.c中的fork,duppage和pgfault函数。

fork函数的实现:高层的COW调用

envid_t
fork(void)
{
    // LAB 4: Your code here.
    set_pgfault_handler(pgfault);
    envid_t envid = sys_exofork();
    if (envid < 0)
        panic("fork: sys_exofork failed.");
    if (envid == 0) {
        thisenv = envs + ENVX(sys_getenvid());
        return 0;
    }
     uint32_t addr;
    for (addr = 0; addr < USTACKTOP; addr += PGSIZE)
        if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) )
            duppage(envid, PGNUM(addr));

    if (sys_page_alloc(envid, (void *)(UXSTACKTOP - PGSIZE), PTE_U | PTE_W | PTE_P) < 0)
        panic("fork: sys_page_alloc failed.");

    sys_env_set_pgfault_upcall(envid, _pgfault_upcall);

    if (sys_env_set_status(envid, ENV_RUNNABLE) < 0)
        panic("fork: sys_env_set_status failed.");

    return envid;
    //panic("fork not implemented");
}

duppage函数的实现:实现内存共享

//
// Map our virtual page pn (address pn*PGSIZE) into the target envid
// at the same virtual address.  If the page is writable or copy-on-write,
// the new mapping must be created copy-on-write, and then our mapping must be
// marked copy-on-write as well.  (Exercise: Why do we need to mark ours
// copy-on-write again if it was already copy-on-write at the beginning of
// this function?)
//
// Returns: 0 on success, < 0 on error.
// It is also OK to panic on error.
//
static int
duppage(envid_t envid, unsigned pn)
{
    int r;

    // LAB 4: Your code here.

    void *addr = (void *)(pn * PGSIZE);
    if((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)){
        if(sys_page_map(0, addr, envid, addr, PTE_U | PTE_COW | PTE_P) < 0)
            panic("duppage: parent->child sys_page_map failed.");
        if(sys_page_map(0, addr, 0, addr, PTE_U | PTE_COW | PTE_P) < 0)
            panic("duppage: self sys_page_map failed.");
    } else {
        if(sys_page_map(0, addr, envid, addr, PTE_P | PTE_U) < 0)
            panic("duppage: single sys_page_map failed.");
    }
    return 0;
}

pgfault函数的实现:一旦发生冲突,就开始物理页的复制分离。

static void
pgfault(struct UTrapframe *utf)
{
    void *addr = (void *) utf->utf_fault_va;
    //cprintf("fault va = %x\n", utf->utf_fault_va);
    uint32_t err = utf->utf_err;
    int r;

    // Check that the faulting access was (1) a write, and (2) to a
    // copy-on-write page.  If not, panic.
    // Hint:
    //   Use the read-only page table mappings at uvpt
    //   (see <inc/memlayout.h>).

    // LAB 4: Your code here.

    // Allocate a new page, map it at a temporary location (PFTEMP),
    // copy the data from the old page to the new page, then move the new
    // page to the old page's address.
    // Hint:
    //   You should make three system calls.

    // LAB 4: Your code here.


    if(!(err & FEC_WR) || !(uvpt[PGNUM(addr)] & PTE_COW))
        panic("pgfault: invalid UTrapFrame");

    envid_t envid = sys_getenvid();
    addr = ROUNDDOWN(addr, PGSIZE);
    // if(envid == 4097){
    //     //cprintf("%x\n", addr);
    //     int val = sys_page_map(4097, addr, 4097, addr, PTE_U | PTE_W | PTE_P);
    //     cprintf("return erro %d\n", val);
    //     return ;
    // }
    //cprintf("now allocating page: envid %d \n", envid);
    if(sys_page_alloc(envid, PFTEMP, PTE_U | PTE_W | PTE_P) < 0)
        panic("pgfault: sys_page_alloc failed.");
    memcpy(PFTEMP, addr, PGSIZE);
    if(sys_page_map(envid, PFTEMP, envid, addr, PTE_U | PTE_W | PTE_P) < 0)
        panic("pgfault: sys_page_map failed.");
    if(sys_page_unmap(envid, PFTEMP) < 0)
        panic("pgfault: sys_page_unmap failed.");
    //panic("pgfault not implemented");
}

_pgfault_upcall是一个通用的函数调用过程,使得程序能够从用户态陷入到异常处理栈部分。

//陷入的过程中进行调用
tf->tf_eip = (uint32_t)curenv->env_pgfault_upcall;

_pgfault_handler部分是用户能够自由定义的page fault处理函数,在已经陷入到异常处理栈进行调用。

_pgfault_upcall:
    // Call the C page fault handler.
    pushl %esp            // function argument: pointer to UTF
    movl _pgfault_handler, %eax 
    call *%eax                //这里进行调用
    addl $4, %esp            // pop function argument

有个问题:这样不会造成页面的浪费吗?因为一开始该页设置为只读,每一个尝试修改后,都会复制新页。并没有区分child process还是parent process。(需要进行实锤)

为了验证上面的想法,我们在构造函数/user/forktwice.c,并且在pgfault()函数和sys_page_map()(硬编码代码很丑)中进行相应的修改,由于该程序是parent先发生页错误中断,然后child process放生页错误中断。我们在第二次发生中断后直接将该页面改为可写。发现程序是能正确的运行的,发现确实有多余的页分配。

最后想想,发现这样提升效率并不对。设想在parent process中进行10次fork()并不知道谁最后一个获得该页面的权限,因此是一个空间换时间的操作。

Part C: Preemptive Multitasking and Inter-Process communication (IPC)

Clock Interrupts and Preemption

目前JOS系统中,需要进程主动交出运行的权限,才能切换进程。如果用户进程使用while死循环,那么其他的进程和内核将永运都不能执行,这将是致命的。

为了能够让kernel主动的夺回使用权,我们需要使得JOS支持外部硬件的时钟中断。

Interrupt discipline

外界中断(IRQs)有16个,在IDT表中的位置为IRQ_OFFSETIRQ_OFFSET+15.

在JOS的设置中IRQ_OFFSET=32。这样设置,是为了不让和processor exceptions重叠。

在JOS中,我们还做了简化:在用户态可以激发硬件中断,但是在内核态,我们会关闭硬件中断。

外界中断主要是通过eflagsFL_IF进行设置的,我们发现在env_alloc初始的时候,确实新增了这样的代码,使得能够在用户态产生硬件中断:

// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;

exercise 13

修改kern/trapentry.Skern/trap.c,增加硬件中断的入口。

sched_halt增加sti,取消在内核态的中断。

TRAPHANDLER_NOEC(handler_timer,    IRQ_OFFSET + IRQ_TIMER)
TRAPHANDLER_NOEC(handler_kbd,      IRQ_OFFSET + IRQ_KBD)
TRAPHANDLER_NOEC(handler_serial,   IRQ_OFFSET + IRQ_SERIAL)
TRAPHANDLER_NOEC(handler_spurious, IRQ_OFFSET + IRQ_SPURIOUS)
TRAPHANDLER_NOEC(handler_ide,      IRQ_OFFSET + IRQ_IDE)
TRAPHANDLER_NOEC(handler_error,    IRQ_OFFSET + IRQ_ERROR)
SETGATE(idt[IRQ_OFFSET + IRQ_TIMER],    0, GD_KT, handler_timer,    0);
SETGATE(idt[IRQ_OFFSET + IRQ_KBD],      0, GD_KT, handler_kbd,      0);
SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL],   0, GD_KT, handler_serial,   0);
SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, handler_spurious, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_IDE],      0, GD_KT, handler_ide,      0);
SETGATE(idt[IRQ_OFFSET + IRQ_ERROR],    0, GD_KT, handler_error,    0);

最后,在kern/env.c 的env_alloc() 中加入e->env_tf.tf_eflags |= FL_IF; 在用户态能进行用户中断,以使得中断发生时内核能够正确处理,并取消kern/sched.c 中sti 的注释。

Handling Clock Interrupts

目前lapic_initpic_init初始化了硬件中断的条件,使得硬件能够产生硬件中断并且能够被系统接收到。下面我们需要实现处理这些中断。

exercise 14

修改内核的trap_dispatch 函数,让它调用sched_yield() 来在时钟中断发生时查找并运行一个不同的环境。

要处理时钟中断,只需要在trap_dispatch 中添加如下代码即可。注意需要使用lapic_eoi() 来接受中断。

// Handle clock interrupts. Don't forget to acknowledge the
    // interrupt using lapic_eoi() before calling the scheduler!
    // LAB 4: Your code here.
    if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
        lapic_eoi();
        sched_yield();
    }

Inter-Process communication (IPC)

之前所有做的工作,使得进程像是单独使用机器资源的假象。

操作系统提供的另外一个服务就是程序间的通信。这是一个强有力的能力,比如Unix pipe就是一个很典型的例子。

IPC in JOS

能够传递一个32位的数字和一页的映射。使得通信的进程共享一些页内容。

Sending and Receiving Messages

为了能够收到信息,接受environment会将自己的状态设置为ENV_NOT_RUNNALBEL并且主动使用sys_yield()主动交出CPU使用权限。

为了能够主动的发送一个值,进程会使用sys_ipc_try_send去发送,包含的参数是目标进程号。通过判断目标进程状态,看看是否处于接受信息的状态:env->env_ipc_recving

若对方成功接收了或者对方压根没有接受,那么直接返回,否则其他情况发送进程处于忙轮询状态(while(1))。

sys_ipc_recvipc_recv调用,ipc_recv供用户的调用。

同理sys_ipc_sendipc_recv调用。

Transferring Pages

sys_ipc_try_send传递一个合理的需要传输的页虚拟源地址,内核首先找到物理页,然后将接受environment目标接受的区域(dstva默认初始化为UTOP,或者调用的时候指定)进行映射。

从而达到共享页的目的。

Implementing IPC

exercise 15

实现在kern/syscall.c中的sys_ipc_recvsys_ipc_try_send。注意将调用envid2env 中的checkperm flag设置为0,使得任何environment都能发送消息。

之后实现在lib/ipc.c中的ipc_recvipc_send

首先实现sys_ipc_recv。需要注意的是,如果参数指示的虚拟地址大于UTOP 时不能报错退出,而需要忽略,因为这只说明接受者只需要接收值而不需要共享页面。实现的代码如下:

static int
sys_ipc_recv(void *dstva)
{
    // LAB 4: Your code here.
    if((uint32_t)dstva < UTOP && PGOFF(dstva))
        return -E_INVAL;
    curenv->env_ipc_recving = true;
    curenv->env_ipc_dstva = dstva;
    curenv->env_status = ENV_NOT_RUNNABLE;
    sys_yield();
    return 0;
    //panic("sys_ipc_recv not implemented");
}

接下来实现sys_ipc_try_send,需求与sys_page_map 类似,但是不能直接调用sys_page_map,因为sys_page_map 会检查env 的权限。实现的sys_ipc_try_send 如下:

static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
    // LAB 4: Your code here.
    struct Env *env;
    if(envid2env(envid, &env, 0) < 0)
        return -E_BAD_ENV;
    if(!env->env_ipc_recving)
        return -E_IPC_NOT_RECV;

    pte_t *pte;
    int res;
    struct PageInfo *page = page_lookup(curenv->env_pgdir, srcva, &pte);
    if((uint32_t)srcva < UTOP && (
                PGOFF(srcva) || !page ||
                (perm & PTE_W) != (*pte & PTE_W) ||
                !(perm & PTE_U) || !(perm & PTE_P) ||
                (perm & (~PTE_SYSCALL))) )
            return -E_INVAL;
    if((uint32_t)srcva < UTOP && (res = page_insert(env->env_pgdir, page, env->env_ipc_dstva, perm)) < 0)
        return res;

    env->env_ipc_recving = 0;
    env->env_ipc_from = curenv->env_id;
    env->env_ipc_perm = perm;
    env->env_ipc_value = value;
    env->env_status = ENV_RUNNABLE;
    return 0;
    //panic("sys_ipc_try_send not implemented");
}

接下来实现对于上面两个系统调用的封装。首先是ipc_recv,值得注意的是当不需要共享页面时将虚拟地址设置为大于或等于UTOP 的值。ipc_recv 实现的代码如下:

int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
    // LAB 4: Your code here.
    if(!pg)
        pg = (void *)UTOP;
    int res;
    if((res = sys_ipc_recv(pg)) < 0){
        if(from_env_store)
            *from_env_store = 0;
        if(perm_store)
            *perm_store = 0;
        return res;
    }
    if(from_env_store)
        *from_env_store = thisenv->env_ipc_from;
    if(perm_store)
        *perm_store = thisenv->env_ipc_perm;
    return thisenv->env_ipc_value;
    return 0;
    //panic("ipc_recv not implemented");
}

然后是ipc_send,同样,当pg 为NULL 时设置虚拟地址为大于或等于UTOP 的值。ipc_send实现如下:

void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
    // LAB 4: Your code here.
     if(!pg)
        pg = (void *)UTOP;
    int res;
    while(1){
        res = sys_ipc_try_send(to_env, val, pg, perm);
        if(!res)
            return;
        if(res != -E_IPC_NOT_RECV)
            panic("ipc_send: not receiving");
        sys_yield();
    }
    //panic("ipc_send not implemented");
}

最后加入系统调用:

        case SYS_ipc_try_send:
            return sys_ipc_try_send(a1, a2, (void *)a3, a4);
        case SYS_ipc_recv:
            return sys_ipc_recv((void *)a1);

番外:硬件中断是怎么进行的

我们可以看到,init_i386中有一个调用:

pic_init();

但是整个实验的篇幅很少涉及到这部分的代码,主要这一部分是对可编程中断硬件8259进行的初始化。

这里面还是大有学问的,如果对这部分比较感兴趣的同学,其实可以看看自己大学本科学习的《微机原理》里面的资料,一定会有新的发现和认识。这里就不具体进行细致的程序分析了。

results matching ""

    No results matching ""